#
# File: VTKView.py
#
# Description: Class definition of the VTK component.
#			  It acts as an interface to VTK.
#
# Author: Jalmar Teeuw and John Donker
# Date created: 12 October 2008
# Date modified: 9 December 2008
#


# import external libraries
import os
import wx
import vtk

#TODO: remove
import time, traceback

#from VTKController import *
from VTKInteractor import *
from wx.lib.pubsub import Publisher


# class definition
# extends from wxWindow
class VTKView(wx.Panel):
	
	# TODO: remove
	VIEW_ID = 1
	
	# constructor
	# -----------
	# dscr:	-
	# req:	-
	# ass:	-
	# ret:	n.a.
	def __init__(self, model, parent, id = -1):
		'''
			Construct a new VTK View
			Configures the RenderWindow and Renderer with default settings
			Subscribes to events
			
			@param model: a reference to the model
			@param parent: the parent to the VTK View (usually a VTK Component)
			@param id: the id for the VTK View
		'''
		# set view id
		self.view_id = self.__class__.VIEW_ID
		self.__class__.VIEW_ID = self.__class__.VIEW_ID + 1
		
		### wx.Window initialization
		
		# get attributes
		position = wx.DefaultPosition
		size = wx.DefaultSize
		style = wx.WANTS_CHARS | wx.NO_FULL_REPAINT_ON_RESIZE
		
		# call super constructor
		wx.Panel.__init__(self, parent, id, position, size, style)
		
		# set window background to black
		self.SetBackgroundColour(wx.Colour(0,0,0))
		
		# set focus to parent panel
		self.SetFocus()
		
		### vtk component initialization
		
		# set properties
		self.__handle = None
		self.__has_painted = False
		
		# create render window
		self._RenderWindow = vtk.vtkRenderWindow()
		
		# configure render window
		#self._RenderWindow.DoubleBufferOn()
		#self._RenderWindow.SwapBuffersOn()
		#self._Renderer.BackingStoreOn()
		#self._RenderWindow.PolygonSmoothingOn()
		#self._RenderWindow.SetAAFrames(8)
		#self._RenderWindow.SetMultiSamples(4)
		self._RenderWindow.SetMultiSamples(0)
		
		self._RenderWindow.SetSize(size.width, size.height)
		
		# create renderer
		self._Renderer = vtk.vtkRenderer()
		
		self._disableRenderer = False
		
		# configure renderer
		#culler = vtk.vtkFrustumCoverageCuller()
		#culler.SetMinimumCoverage(0.4)
		#culler.SetMaximumCoverage(1.0)
		#self._Renderer.AddCuller(culler)
		#self._RenderWindow.SetAlphaBitPlanes(1)
		#self._Renderer.UseDepthPeelingOn()
		#self._Renderer.SetOcclusionRatio(0.10) # increase to 1.00 to speed up rendering (depth peeling)
		#self._Renderer.AutomaticLightCreationOn()
		#self._Renderer.TwoSidedLightingOn()
		#self._Renderer.LightFollowCameraOn()
		
		self._Renderer.SetBackground(0.91, 0.99, 1.00) # Light Blueish
		self._Renderer.SetViewport(0.00, 0.00, 1.00, 1.00) # Full
		self._RenderWindow.AddRenderer(self._Renderer)
		
		# add view label
		self._label = vtk.vtkTextActor()
		self._label.SetDisplayPosition(10, 10)
		self._label.SetInput("3D View (P)") # NOTE: view is 3D Perspective by default (unconstrained)
		#self._label.SetInput("3D Perspective View") # NOTE: view is 3D Perspective by default (unconstrained)
		#self._label.VisibilityOff() # NOTE: do not enable by default
		
		txt_prop = self._label.GetTextProperty()
		txt_prop.SetColor(0, 0, 0) # Black
		
		self._Renderer.AddActor2D(self._label)
		
		# position / restrict camera
		self._Camera = self._Renderer.GetActiveCamera()
		#self._Camera.SetPosition(0.00, 0.00, 20.00)
		
		# bind events
		self.Bind(wx.EVT_SIZE, self.OnSize)
		self.Bind(wx.EVT_PAINT, self.OnPaint)

		# listen to publisher / subscriber events
		Publisher.subscribe(self._AddGeom, "GeomAdded")
		Publisher.subscribe(self._AddGeom, "DicomAdded")
		Publisher.subscribe(self._AddGeom, "InstrumentAdded")
		Publisher.subscribe(self._RemoveGeom, "GeomRemoved")
		Publisher.subscribe(self._RemoveGeom, "DicomRemoved")
		Publisher.subscribe(self._RemoveGeom, "InstrumentRemoved")
		Publisher.subscribe(self._UpdateView, "GeomUpdated")
		Publisher.subscribe(self._UpdateView, "DicomUpdated")
		Publisher.subscribe(self._UpdateView, "InstrumentUpdated")
		
		# create render window interactor # NOTE: keep these as last instructions
		self._RenderWindowInteractor = VTKInteractor(model, self)
		
		self.__geomsAdded = 0

	
	#####
	# Properties
	#####
	
	# set the view port label's text input
	# -----------
	# dscr:	-
	# req:	-
	# ass:	-
	# ret:	n.a.
	def SetViewportLabel(self, txt):
		'''
			Set the label of the view (in the lower left corner)
			
			@param txt: the text for the label
		'''
		self._label.SetInput(txt)
		
	
	# enable the viewport label
	# -----------
	# dscr:	-
	# req:	-
	# ass:	-
	# ret:	n.a.
	def EnableViewportLabel(self):
		'''
			Make the viewport label visible
		'''
		self._label.VisibilityOn()
	
	
	# disable the viewport label
	# -----------
	# dscr:	-
	# req:	-
	# ass:	-
	# ret:	n.a.
	def EnableViewportLabel(self):
		'''
			Hide the viewport label
		'''
		self._label.VisibilityOff()
		
		
	# get a reference to the current render window
	# -----------
	# dscr:	-
	# req:	-
	# ass:	-
	# ret:	n.a.
	def GetRenderWindow(self):
		'''
			Get a reference to the RenderWindow
			
			@return: the RenderWindow used by VTK View
		'''
		return self._RenderWindow
	
	# get a reference to the current renderer
	# -----------
	# dscr:	-
	# req:	-
	# ass:	-
	# ret:	n.a.
	def GetRenderer(self):
		'''
			Get a reference to the Renderer
			
			@return: the Renderer used by VTK View
		'''
		return self._Renderer
		
	
	# get a reference to the current active camera
	# -----------
	# dscr:	-
	# req:	-
	# ass:	-
	# ret:	n.a.
	def GetCamera(self):
		'''
			Get a reference to the Camera
			
			@return: the Camera used by VTK View (in the Renderer)
		'''
		return self._Camera
	
	
	# get a reference to the current render window interactor
	# -----------
	# dscr:	-
	# req:	-
	# ass:	-
	# ret:	n.a.
	def GetRenderWindowInteractor(self):
		'''
			Get a reference to the RenderWindowInteractor
			
			@return: the RenderWindowInteractor used by VTK View
		'''
		return self._RenderWindowInteractor
	
	
	#####
	# VTK Renderer manipulation
	#####
	
	# enable or disable double buffering
	# -----------
	# dscr:	-
	# req:	-
	# ass:	-
	# ret:	n.a.
	def SetDoubleBuffering(self, flag):
		'''
			Enable or disable double buffering
			
			@param flag: true will enable double buffering, false will disable double buffering
		'''
		if(flag):
			self._RenderWindow.DoubleBufferOn()
			self._RenderWindow.SwapBuffersOn()
			#self._Renderer.BackingStoreOn() # WARN: Causes artifacts!
		else:
			self._RenderWindow.DoubleBufferOff()
			self._RenderWindow.SwapBuffersOff()
			#self._Renderer.BackingStoreOff() # WARN: Causes artifacts!
	
	
	# enable or disable depth peeling
	# -----------
	# dscr:	-
	# req:	-
	# ass:	-
	# ret:	n.a.
	def SetDepthPeeling(self, flag):
		'''
			Enable or disable depth peeling (only if the hardware supports it)
			
			@param flag: true will enable depth peeling, false will disable depth peeling
		'''
		if(flag):
			self._RenderWindow.SetAlphaBitPlanes(True)
			self._Renderer.UseDepthPeelingOn()
			self._Renderer.SetMaximumNumberOfPeels(20)
			self._Renderer.SetOcclusionRatio(0.10) # increase to 1.00 to speed up rendering (depth peeling)
		else:
			self._RenderWindow.SetAlphaBitPlanes(False)
			self._Renderer.UseDepthPeelingOff()
			self._Renderer.SetOcclusionRatio(1.00) # increase to 1.00 to speed up rendering (depth peeling)
	
	
	# enable or disable anti-aliasing
	# -----------
	# dscr:	-
	# req:	-
	# ass:	-
	# ret:	n.a.
	def SetAntiAliasing(self, flag):
		'''
			Enable or disable anti-aliasing (seriously decreases the performance!!)
			
			@param flag: true will enable anti-aliasing, false will disable anti-aliasing
		'''
		if(flag):
			self._RenderWindow.PointSmoothingOn()
			self._RenderWindow.LineSmoothingOn()
			self._RenderWindow.PolygonSmoothingOn()
			self._RenderWindow.SetAAFrames(8)
			self._RenderWindow.SetMultiSamples(4)
		else:
			self._RenderWindow.PointSmoothingOff()
			self._RenderWindow.LineSmoothingOff()
			self._RenderWindow.PolygonSmoothingOff()
			self._RenderWindow.SetAAFrames(0)
			self._RenderWindow.SetMultiSamples(0)
	
	
	#####
	# VTK Camera manipulation
	#####
	
	# set perspective view
	# -----------
	# dscr:	-
	# req:	-
	# ass:	-
	# ret:	n.a.
	def SetPerspectiveView(self):
		'''
			Set the viewport in perspective mode
		'''
		self._Camera.ParallelProjectionOff()
		self.Render()
	
	
	# set orthogonal (or parallel) view
	# -----------
	# dscr:	-
	# req:	-
	# ass:	-
	# ret:	n.a.
	def SetOrthogonalView(self):
		'''
			Set the viewport in orthogonal mode
		'''
		self._Camera.ParallelProjectionOn()
		self.Render()
	
		
	# set left view
	# -----------
	# dscr:	-
	# req:	-
	# ass:	-
	# ret:	n.a.
	def SetLeftView(self):
		'''
			Set the viewport in left-side view mode
		'''
		self._Camera.SetPosition(-20.00, 0.00, 0.00)
		self._Camera.SetFocalPoint(0.00, 0.00, 0.00)
		self._Camera.SetViewUp(0.00, 1.00, 0.00)
		
		self.SetOrthogonalView()
		
		self.SetViewportLabel("Left View")
		
		self.ResetCamera()
		
		self.GetRenderWindowInteractor().SetInteractorStyle(vtk.vtkInteractorStyleImage())
	
	
	# set right view
	# -----------
	# dscr:	-
	# req:	-
	# ass:	-
	# ret:	n.a.
	def SetRightView(self):
		'''
			Set the viewport in right-side view mode
		'''
		self._Camera.SetPosition(20.00, 0.00, 0.00)
		self._Camera.SetFocalPoint(0.00, 0.00, 0.00)
		self._Camera.SetViewUp(0.00, 1.00, 0.00)
		
		self.SetOrthogonalView()
		
		self.SetViewportLabel("Right View")
		
		self.ResetCamera()
		
		self.GetRenderWindowInteractor().SetInteractorStyle(vtk.vtkInteractorStyleImage())
	
	
	# set top view
	# -----------
	# dscr:	-
	# req:	-
	# ass:	-
	# ret:	n.a.
	def SetTopView(self):
		'''
			Set the viewport in top-side view mode
		'''
		self._Camera.SetPosition(0.00, 20.00, 0.00)
		self._Camera.SetFocalPoint(0.00, 0.00, 0.00)
		self._Camera.SetViewUp(0.00, 0.00, 1.00)
		
		self.SetOrthogonalView()
		
		self.SetViewportLabel("Top View")
		
		self.ResetCamera()
		
		self.GetRenderWindowInteractor().SetInteractorStyle(vtk.vtkInteractorStyleImage())
		
	
	# set bottom view
	# -----------
	# dscr:	-
	# req:	-
	# ass:	-
	# ret:	n.a.
	def SetBottomView(self):
		'''
			Set the viewport in bottom-side view mode
		'''
		self._Camera.SetPosition(00.00, -20.00, 0.00)
		self._Camera.SetFocalPoint(0.00, 0.00, 0.00)
		self._Camera.SetViewUp(0.00, 0.00, 10.00)
		
		self.SetOrthogonalView()
		
		self.SetViewportLabel("Bottom View")
		
		self.ResetCamera()
		
		self.GetRenderWindowInteractor().SetInteractorStyle(vtk.vtkInteractorStyleImage())
		
	
	# set front view
	# -----------
	# dscr:	-
	# req:	-
	# ass:	-
	# ret:	n.a.
	def SetFrontView(self):
		'''
			Set the viewport in front-side view mode
		'''
		self._Camera.SetPosition(0.00, 0.00, 20.00)
		self._Camera.SetFocalPoint(0.00, 0.00, 0.00)
		self._Camera.SetViewUp(0.00, 1.00, 0.00)
		
		self.SetOrthogonalView()
		
		self.SetViewportLabel("Front View")
		
		self.ResetCamera()
		
		self.GetRenderWindowInteractor().SetInteractorStyle(vtk.vtkInteractorStyleImage())
		
	
	# set back view
	# -----------
	# dscr:	-
	# req:	-
	# ass:	-
	# ret:	n.a.
	def SetBackView(self):
		'''
			Set the viewport in back-side view mode
		'''
		self._Camera.SetPosition(0.00, 0.00, -20.00)
		self._Camera.SetFocalPoint(0.00, 0.00, 0.00)
		self._Camera.SetViewUp(0.00, 1.00, 0.00)
		
		self.SetOrthogonalView()
		
		self.SetViewportLabel("Back View")
		
		self.ResetCamera()
		
		self.GetRenderWindowInteractor().SetInteractorStyle(vtk.vtkInteractorStyleImage())
		
	
	# set 3D view
	# -----------
	# dscr:	-
	# req:	-
	# ass:	-
	# ret:	n.a.
	def Set3DView(self):
		'''
			Set the viewport in 3D perspective view mode
			
			@deprecated: use Set3DPerspectiveView() or Set3DOrthogonalView() instead!
		'''
		
		print "Deprecated: use Set3DPerspectiveView() or Set3DOrthogonalView() instead!"
		
		# default to perspective
		Set3DPerspectiveView()
	
	
	# set 3D Perspective view
	# -----------
	# dscr:	-
	# req:	-
	# ass:	-
	# ret:	n.a.
	def Set3DPerspectiveView(self):
		'''
			Set the viewport in 3D perspective view mode
		'''
		self._Camera.SetPosition(5.00, 5.00, 5.00)
		self._Camera.SetFocalPoint(0.00, 0.00, 0.00)
		self._Camera.SetViewUp(0.00, 1.00, 0.00)
		
		self.SetPerspectiveView()
		
		self.SetViewportLabel("3D View (P)")
		#self.SetViewportLabel("3D Perspective View")
		
		self.ResetCamera()
		
		self.GetRenderWindowInteractor().SetInteractorStyle(vtk.vtkInteractorStyleTrackballCamera())
		
	
	# set 3D Orthogonal view
	# -----------
	# dscr:	-
	# req:	-
	# ass:	-
	# ret:	n.a.
	def Set3DOrthogonalView(self):
		'''
			Set the viewport in 3D orthogonal view mode
		'''
		self._Camera.SetPosition(5.00, 5.00, 5.00)
		self._Camera.SetFocalPoint(0.00, 0.00, 0.00)
		self._Camera.SetViewUp(0.00, 1.00, 0.00)
		
		self.SetOrthogonalView()
		
		self.SetViewportLabel("3D View (O)")
		#self.SetViewportLabel("3D Orthogonal View")
		
		self.ResetCamera()
		
		self.GetRenderWindowInteractor().SetInteractorStyle(vtk.vtkInteractorStyleTrackballCamera())
		
	
	# reset camera view, center on all actors in renderer
	# -----------
	# dscr:	-
	# req:	-
	# ass:	-
	# ret:	n.a.
	def ResetCamera(self):
		'''
			Reset the camera to a point where all geoms are visible
		'''
		self._Camera.SetFocalPoint(0.00, 0.00, 0.00) # focus on world's origin
		self._Renderer.ResetCamera()
		self.Render()
	
	
	#####
	# VTK Window events
	#####
	
	# disable the renderer
	# -----------
	# dscr:	-
	# req:	-
	# ass:	-
	# ret:	n.a.
	def DisableRenderer(self):
		'''
			Disables the updating of the renderer
		'''
		self._disableRenderer = True
	
	
	# enable the renderer
	# -----------
	# dscr:	-
	# req:	-
	# ass:	-
	# ret:	n.a.
	def EnableRenderer(self):
		'''
			Enables the updating of the renderer
		'''
		self._disableRenderer = False
	
	
	# force an update of the view
	# -----------
	# dscr:	-
	# req:	-
	# ass:	-
	# ret:	n.a.
	def Render(self):
		'''
			Update the view in the viewport
		'''
		if not self._disableRenderer:
			
			#print("Render " + str(self.view_id) + " not disabled!") #temp
			
			if(self.__handle and self.__handle == self.GetHandle()):
				
				self._Renderer.ResetCameraClippingRange()
				#self._Camera.SetClippingRange(500.0, 1000.0)
				self._RenderWindow.Render()
				
			elif self.GetHandle() and self.__has_painted:
			
				self._RenderWindow.SetNextWindowInfo(str(self.GetHandle()))
				
				d = self.GetDisplayId()
				if d:
					self._RenderWindow.SetDisplayId(d)
				
				self._RenderWindow.WindowRemap()
				
				self.__handle = self.GetHandle()
				
				self._Renderer.ResetCameraClippingRange()
				#self._Camera.SetClippingRange(500.0, 1000.0)
				self._RenderWindow.Render()
			
		# TODO: remove
		#print "    took " + str(self._Renderer.GetLastRenderTimeInSeconds()) + " seconds"
		
	
	# get display id, for *nix systems
	# -----------
	# dscr:	-
	# req:	-
	# ass:	-
	# ret:	n.a.
	def GetDisplayId(self):
		'''
			Get the ID of the display (on Unix systems)
			
			@note: this is a private function and should not be called by the user
		'''
		# get display id string
		d = None
		try:
			d = wx.GetXDisplay()
		
		except NameError:
			pass
		
		# format display id string
		if d:
			d = hex(d)
			
			if not d.startswith("0x"):
				d = "0x" + d
			
			d = "_%s_%s" % (d[2:], "void_p")
		
		# return display id string
		return d
	
	
	# on paint event
	# -----------
	# dscr:	-
	# req:	-
	# ass:	-
	# ret:	n.a.
	def OnPaint(self, event):
		'''
			Trigger a paint event to update the viewport
			
			@param event: window paint event
			
			@note: this is a private function and should not be called by the user
		'''
		# forward event
		event.Skip()
		
		# get context
		dc = wx.PaintDC(self)
		
		# size render window
		self._RenderWindow.SetSize(self.GetSizeTuple())
		self._RenderWindowInteractor.SetSize(self.GetSizeTuple())
		
		# render to window
		if not self.__handle:
		
			# on relevant platorms, set the X11 Display ID
			d = self.GetDisplayId()
			if d:
				self._RenderWindow.SetDisplayId(d)
			
			# store the handle
			self.__handle = self.GetHandle()
			#and give it to VTK
			self._RenderWindow.SetWindowInfo(str(self.__handle))
			
			# now that we've painted once, the Render() reparenting logic is safe
			self.__has_painted = True
		
		# render screen
		self.Render()
	
	
	# on size event
	# -----------
	# dscr:	-
	# req:	-
	# ass:	-
	# ret:	n.a.
	def OnSize(self, event):
		'''
			Trigger a resize event and update the viewport
			
			@param event: window size event
			
			@note: this is a private function and should not be called by the user
		'''
		# forward event
		event.Skip()
		
		# set new size of render window AND render window interactor
		self._RenderWindow.SetSize(event.GetSize().width, event.GetSize().height)
		self._RenderWindowInteractor.SetSize(event.GetSize().width, event.GetSize().height)
		self._RenderWindowInteractor.ConfigureEvent()
		
		"""
		# resize viewport of omw's
		rel_size_w = 100.0 / event.GetSize().width # always 100 px
		rel_size_h = 100.0 / event.GetSize().height
		
		#self._omw_axes.SetViewport(0.00, 0.00, rel_size_w, rel_size_h) # bottom left
		self._omw_cube.SetViewport(1.00 - rel_size_w, 0.00, 1.00, rel_size_h) # bottom right
		"""
		
		# render screen
		self.Render()

	
	#####
	# Publisher / Subscriber Events
	#####
	
	# Adds an actor to the renderer
	# -----------
	# dscr:	-
	# req:	-
	# ass:	-
	# ret:	n.a.
	def _AddGeom(self, message):
		'''
			Add a geom to the renderer
			
			@param message: event message
			
			@note: this is a private function and should not be called by the user
		'''
		# get data
		geom = message.data
		
		# create depth sorter
		ds = vtk.vtkDepthSortPolyData()
		ds.SetProp3D(geom.GetActor())
		ds.SetDirectionToBackToFront()
		ds.SetCamera(self.GetCamera())
		
		# create new box widget
		bw = vtk.vtkBoxWidget()
		bw.SetPlaceFactor(1.00)
		bw.SetProp3D(geom.GetActor())
		bw.PlaceWidget()
		bw.SetInteractor(self.GetRenderWindowInteractor())
		bw.AddObserver("StartInteractionEvent", geom._StartInteractionCallback)
		bw.AddObserver("InteractionEvent", geom._InteractionCallback)
		bw.AddObserver("EndInteractionEvent", geom._EndInteractionCallback)
		
		# add box widget to list
		geom.AddBoxWidget(bw)
		
		# add actor and axes
		self._Renderer.AddActor(geom.GetActor())
		#self._Renderer.AddActor(geom.GetAxes())
		
		# counter
		if self.__geomsAdded == 0:
			self.ResetCamera()
		self.__geomsAdded = self.__geomsAdded + 1
		
		#print("_AddGeom() called") #temp
		# update view
		self.Render()

		
	# Removes an actor from the renderer
	# -----------
	# dscr:	-
	# req:	-
	# ass:	-
	# ret:	n.a.
	def _RemoveGeom(self, message):
		'''
			Remove a geom from the renderer
			
			@param message: event message
			
			@note: this is a private function and should not be called by the user
		'''
		# get data
		geom = message.data
		
		# disable box widgets (just to be sure)
		geom.DisableBoxWidgets()
		
		# remove actor and axes
		self._Renderer.RemoveActor(geom.GetActor())
		#self._Renderer.RemoveActor(geom.GetAxes()) # :D
		
		# TODO: unbind boxwidget from interactor?
		
		self.__geomsAdded = self.__geomsAdded - 1
		
		#print("_RemoveGeom() called") #temp
		# update view
		self.Render()
	
	
	# Update the render view
	# -----------
	# dscr:	-
	# req:	-
	# ass:	-
	# ret:	n.a.
	def _UpdateView(self, message):
		'''
			Update the view
			
			@param message: event message
			
			@note: this is a private function and should not be called by the user
		'''
		self.Render()
